package info.motteke.mybws.script;

import info.motteke.mybws.utils.ListReverser;

import java.lang.reflect.InvocationTargetException;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.Map;

import org.apache.commons.beanutils.ConvertUtilsBean;
import org.apache.commons.beanutils.PropertyUtilsBean;

/**
 * 値を保存するスタックです。
 * 
 * @author YWak
 */
public class ValueStack {

    /** 実際に値を保存しているスタック */
    private final LinkedList<NamedValue> stack;

    /** プロパティ操作を行うインスタンス */
    private final PropertyUtilsBean property;

    /** 値の変換を行うインスタンス */
    private final ConvertUtilsBean convert;

    /**
     * インスタンスを生成します。
     */
    public ValueStack() {
        stack = new LinkedList<NamedValue>();

        property = new PropertyUtilsBean();
        convert = new ConvertUtilsBean();
        convert.register(false, false, 0);
    }

    /**
     * スタックに値を積みます。
     * 
     * @param value
     *            値
     */
    public void push(Object value) {
        push(null, value);
    }

    /**
     * 名前つきでスタックに値を積みます。
     * 
     * @param name
     *            名前
     * @param value
     *            値
     */
    public void push(String name, Object value) {
        stack.push(new NamedValue(name, value));
    }

    /**
     * スタックから値を取り出します。
     * スタックから値は取り除かれます。
     * 
     * @return 値
     */
    public Object pop() {
        return stack.pop().getValue();
    }

    /**
     * スタックの値を取り出します。
     * スタックは変更されません。
     * 
     * @return 値
     */
    public Object peek() {
        return stack.peek().getValue();
    }

    /**
     * 式を解釈して、文字列化した値を取り出します。
     * 該当する要素がない場合、nullを返します。
     * 
     * @param expression
     *            式
     * @return expressionが表す値
     */
    public String eval(String expression) {
        return (String) getValue(expression, ValueType.STRING);
    }

    /**
     * 式を解釈して、要素を取り出します。
     * 
     * @param expression
     *            式
     * @return expressionが表す値
     */
    public Object get(String expression) {
        return getValue(expression, ValueType.PROPERTY);
    }

    /**
     * 値を取得します。
     * 
     * @param expression
     *            式
     * @param type
     *            取得対象
     * @return 取得した値。なければnull。
     */
    private Object getValue(String expression, ValueType type) {

        Map<String, Object> namedValues = createNamedValues();

        try {
            // 名前つき要素を積む。
            stack.push(new NamedValue(null, namedValues));

            // スタックの上から順に要素を調べていく。
            for (NamedValue nv : new ListReverser<NamedValue>(stack)) {
                Object value = type.get(this, nv.getValue(), expression);

                if (value != null) {
                    return value;
                }
            }

            return null;
        } finally {
            // 何があっても要素を取り除く。
            NamedValue popped = stack.pop();

            assert popped.getValue() == namedValues;
        }
    }

    /**
     * 名前つき要素のマップを作成する。
     * 同名の要素は、スタックの上が優先される。
     * 
     * @return 名前つき要素のマップ
     */
    private Map<String, Object> createNamedValues() {
        Map<String, Object> map = new LinkedHashMap<String, Object>();

        // スタックの下から取得すると、上が優先になる。
        for (NamedValue nv : stack) {
            if (nv.getName() != null) {
                map.put(nv.getName(), nv.getValue());
            }
        }

        return map;
    }

    /**
     * プロパティを取得します。
     * 
     * @param bean
     *            プロパティを取り出す要素
     * @param name
     *            プロパティ名
     * @return 取得したプロパティ。 なければnull。
     */
    private Object getProperty(Object bean, String name) {
        try {
            return property.getProperty(bean, name);
        } catch (IllegalAccessException e) {
        } catch (NoSuchMethodException e) {
        } catch (InvocationTargetException e) {
        }

        return null;
    }

    /**
     * 値を取得します。
     * 
     * @param bean
     *            プロパティを取り出す要素
     * @param name
     *            プロパティ名
     * @param type
     *            取得する型
     * @return 取得した値。なければnull。
     */
    private <T> T getValue(Object bean, String name, Class<T> type) {

        Object converted = convert.convert(getProperty(bean, name), type);

        if (type.isInstance(converted)) {
            return type.cast(converted);
        } else {
            return null;
        }
    }

    /**
     * 値の取得方法です。
     * 
     * @author YWak
     */
    private enum ValueType {

        /** プロパティを取得します。 */
        PROPERTY() {
            @Override
            public Object get(ValueStack stack, Object bean, String name) {
                return stack.getProperty(bean, name);
            }
        },

        /** 文字列を取得します。 */
        STRING() {
            @Override
            public Object get(ValueStack stack, Object bean, String name) {
                return stack.getValue(bean, name, String.class);
            }
        }

        ;

        /**
         * この列挙子が表す方法で、値を取得します。
         * 
         * @param stack
         *            値を積んだスタック
         * @param bean
         *            値の取得元
         * @param name
         *            プロパティ名
         * @return 取得した値。なければnull
         */
        public abstract Object get(ValueStack stack, Object bean, String name);
    }
}
